package com.beisita.config;

import com.alibaba.fastjson.JSONObject;
import com.alibaba.fastjson.serializer.SerializerFeature;
import com.alibaba.fastjson.support.config.FastJsonConfig;
import com.alibaba.fastjson.support.spring.FastJsonHttpMessageConverter;
import com.beisita.interceptor.CorsInterceptor;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.core.Ordered;
import org.springframework.http.MediaType;
import org.springframework.http.converter.HttpMessageConverter;
import org.springframework.http.converter.StringHttpMessageConverter;
import org.springframework.web.servlet.config.annotation.*;

import java.nio.charset.Charset;
import java.util.ArrayList;
import java.util.List;

/**
 * 自定义部分MVC配置
 *
 * @author Roger
 * @date 2020/10/23 13:06
 */
@Configuration
public class MyWebMvcConfigurer implements WebMvcConfigurer {


    @Value("${uploadFile.resourceHandler}")
    private String resourceHandler;//请求 url 中的资源映射，不推荐写死在代码中，最好提供可配置，如 /uploadFiles/**

    @Value("${uploadFile.location}")
    private String location;//上传文件保存的本地目录，使用@Value获取全局配置文件中配置的属性值，如 E:/wmx/uploadFiles/

    /**
     * 默认静态资源处理器
     * 此时会注册一个默认的Handler：DefaultServletHttpRequestHandler，
     * 这个Handler也是用来处理静态文件的，它会尝试映射/。当DispatcherServelt映射/时（/ 和/ 是有区别的），
     * 并且没有找到合适的Handler来处理请求时，就会交给DefaultServletHttpRequestHandler 来处理。
     * 注意：这里的静态资源是放置在web根目录下，而非WEB-INF 下。
     * 可能这里的描述有点不好懂（我自己也这么觉得），所以简单举个例子，例如：在webroot目录下有一个图片：1.png
     * 我们知道Servelt规范中web根目录（webroot）下的文件可以直接访问的，但是由于DispatcherServlet配置了映射路径是：/ ，
     * 它几乎把所有的请求都拦截了，从而导致1.png 访问不到，这时注册一个DefaultServletHttpRequestHandler 就可以解决这个问题。
     * 其实可以理解为DispatcherServlet破坏了Servlet的一个特性（根目录下的文件可以直接访问），
     * DefaultServletHttpRequestHandler是帮助回归这个特性的。
     */
    @Override
    public void configureDefaultServletHandling(DefaultServletHandlerConfigurer configurer) {
        configurer.enable();
        configurer.enable("defaultServletName");
    }

    @Override
    public void addCorsMappings(CorsRegistry registry) {
        registry.addMapping("/**")    //添加映射路径，“/**”表示对所有的路径实行全局跨域访问权限的设置
                .allowedOrigins("*")    //开放哪些ip、端口、域名的访问权限
                .allowCredentials(true)  //是否允许发送Cookie信息
                .maxAge(3600)
                .allowedMethods("GET", "POST", "PUT", "DELETE")     //开放哪些Http方法，允许跨域访问
                .allowedHeaders("*")     //允许HTTP请求中的携带哪些Header信息
                .exposedHeaders("Authorization", "access-control-allow-headers", "access-control-allow-methods",
                        "access-control-allow-origin", "access-control-max-age", "X-Frame-Options");   //暴露哪些头部信息（因为跨域访问默认不能获取全部头部信息）
    }

    /*    自定义静态资源映射目录的话，只需重写addResourceHandlers方法即可。
        注：如果继承WebMvcConfigurationSupport类实现配置时必须要重写该方法*/
//    https://www.cnblogs.com/pomer-huang/p/pomer.html
/*    1.当不存在 @EnableWebMVC 时
    SpringBoot 的 @EnableAutoConfiguration
    会启用自动配置类 WebMvcAutoConfiguration，该类配置了一些默认的静态资源映射
    自动映射 localhost:8080
    /**
     * 为以下路径
     * classpath:/resources/
     * classpath:/static/
     * classpath:/public/
     * classpath:/META-INF/resources/
     * 自动映射 localhost:8080/webjars/** 为以下路径
     * classpath:/META-INF/resources/webjars/
     * 此时，我们不需要多做什么，只要将静态资源放入 src/main/resources 目录下的 resources、static 或 public 文件夹下，即可通过 url 定位相关资源，例如 localhost:8080/index.html 可定位至 src/main/resources/static/index.html
     * 注意：如果编写了以下的自定义配置，则以上默认配置将被取消。更确切的说，一旦自定义的配置不为空，则默认配置将不被采用。
     * 如果不喜欢代码配置，也可采取以下属性配置方式：
     * spring.resources.static-locations=classpath:/META-INF/resources/,classpath:/resources/,\
     * classpath:/static/,classpath:/public/,file:D://hehe
     * <p>
     * <p>
     * 2.当存在 @EnableWebMVC 时
     * 如果使用了 @EnableWebMvc，则自动配置类 WebMvcAutoConfiguration 会失效，因此默认映射路径 /static, /public, META-INF/resources, /resources 都将失效
     * 这种情况下，只能设置自定义配置
     * 无任何前缀 -> “文档根目录”（一般指代 src/main/webapp 目录）， 例如 localhost:8080/index.html 定位至 src/main/webapp/static/index.html
     * 存在前缀 classpath -> 类路径（一般指代 src/main/resources 目录）
     * 存在前缀 file:// -> 文件系统路径(“绝对路径”)
     * <p>
     * addResoureHandler：指的是对外暴露的访问路径
     * addResourceLocations：指的是内部文件放置的目录
     */
    @Override
    public void addResourceHandlers(ResourceHandlerRegistry registry) {
        //配置静态资源处理
        registry.addResourceHandler("/**")
                .addResourceLocations("resources/", "static/", "public/",
                        "META-INF/resources/")
                .addResourceLocations("classpath:resources/", "classpath:static/",
                        "classpath:public/", "classpath:META-INF/resources/", "classpath:/templates/");
        //请求url与resourceHandler匹配时，则映射到 location 中去,location 相当于虚拟路径
        //映射本地文件时，开头必须是 file:/// 开头，表示协议
        registry.addResourceHandler(resourceHandler).addResourceLocations("file:///" + location);
    }

    /*    SpringMVC的时候，如果需要访问一个页面，必须要写Controller类，
        然后再写一个方法跳转到页面，感觉好麻烦，其实重写WebMvcConfigurer中的addViewControllers方法即可达到效果了
        值的指出的是，在这里重写addViewControllers方法，并不会覆盖WebMvcAutoConfiguration（Springboot自动配置）中的addViewControllers（在此方法中，
        Spring Boot将“/”映射至index.html），这也就意味着自己的配置和Spring Boot的自动配置同时有效，这也是我们推荐添加自己的MVC配置的方式。*/
    @Override
    public void addViewControllers(ViewControllerRegistry registry) {
        registry.addViewController("/").setViewName("/index");//设置默认跳转视图为 /index
        registry.addViewController("/index").setViewName("/index");
        registry.addViewController("/error/500").setViewName("/error/500");
        registry.addViewController("/error/404").setViewName("/error/404");
        registry.setOrder(Ordered.HIGHEST_PRECEDENCE);
    }


    /**
     * 自定义的interceptor中注入其他内容，比如redis或者其他service，必须使用上面这种方法
     * 不能使用registry.addInterceptor(new ...())的方式
     *
     * @return CorsInterceptor
     */
    @Bean
    public CorsInterceptor getCorsInterceptor() {
        return new CorsInterceptor();
    }

    @Override
    public void addInterceptors(InterceptorRegistry registry) {
/*        过滤器和拦截器非常相似，但是它们有很大的区别
        最简单明了的区别就是过滤器可以修改request，而拦截器不能
        过滤器需要在servlet容器中实现，拦截器可以适用于javaEE，javaSE等各种环境
        拦截器可以调用IOC容器中的各种依赖，而过滤器不能
        过滤器只能在请求的前后使用，而拦截器可以详细到每个方法*
        最重要的要记住他们的执行顺序： 先filter 后 interceptor
        过滤前-拦截前-action执行-拦截后-过滤后/
        /**
         * addInterceptor：需要一个实现HandlerInterceptor接口的拦截器实例
         * addPathPatterns：用于设置拦截器的过滤路径规则；addPathPatterns("/**")对所有请求都拦截
         * excludePathPatterns：用于设置不需要拦截的过滤规则
         */

        registry.addInterceptor(getCorsInterceptor())
                .addPathPatterns("/**") //添加拦截路径
                .excludePathPatterns(//添加排除拦截路径
                        "/index",
                        "/login",
                        "/doLogin",
                        "/logout",
                        "/register",
                        "/doRegister",
                        "/**/*.css",
                        "/**/*.png",
                        "/**/*.jpeg",
                        "/**/*.jpg",
                        "/**/*.ico",
                        "/**/*.js",
                        "/css/**",
                        "/images/**",
                        "static/**",
                        "/templates/**",
                        "/swagger-resources/**"
                );

    }

    /**
     * 使用fastjson作为JSON消息转换器
     * 使用StringHttpMessageConverter作为String消息转换器
     * <p>
     * 注: 一般来讲，这两个消息消息转换器就是用于绝大部分情况了;
     * 如果业务环境情况等比较特殊，需要其他的消息转换器，
     * 那么再追加新的转换器即可
     * <p>
     * 注:JSON消息转换器需要引入fastjson依赖
     * StringHttpMessageConverter消息转换器需要引入springframework依赖
     *
     * @date 2020/10/23 13:06
     */
    @Override
    public void configureMessageConverters(List<HttpMessageConverter<?>> converters) {
        /// -> JSON消息转换器(采用阿里的fastjson)
        // 创建一个转换器对象;
        FastJsonHttpMessageConverter converter = new FastJsonHttpMessageConverter();

        // 个性化配置转换特性
        FastJsonConfig config = new FastJsonConfig();
        config.setSerializerFeatures(
                // 要格式化返回的json数据
                SerializerFeature.PrettyFormat,
                // 把空的值的key也返回,保留map空的字段
                SerializerFeature.WriteMapNullValue,
                // 将String类型的null转成""
                SerializerFeature.WriteNullStringAsEmpty,
                // 将Number类型的null转成0 数值字段如果为null,输出为0,而非null
                SerializerFeature.WriteNullNumberAsZero,
                // 将List类型的null转成[] List字段如果为null,输出为[],而非null;
                SerializerFeature.WriteNullListAsEmpty,
                // 将Boolean类型的null转成false，字段如果为null,输出为false,而非null
                SerializerFeature.WriteNullBooleanAsFalse,
                // 避免循环引用
                SerializerFeature.DisableCircularReferenceDetect);
        // fastjson对日期类型的处理 3种方式：
        // 方式一：配置了fastjson作为消息转换器后，在模型的对应日期字段上面使用@JSONField(format = "...")来指定格式。
        // 方式二：在配置fastjson作为消息转换器的使用，通过.setDateFormat("...")指定全局Date字段的序列化格式。
        // 如果使用了.setDateFormat("...")的话，那么模型字段上通过 @JSONField(format = "...")指定日期序列化格式的方
        // 式，将不起作用。
//        config.setDateFormat("yyyy-MM-dd HH:mm:ss");
        // 方式三：在配置fastjson作为消息转换器的时候，使用.setSerializerFeatures(SerializerFeature.WriteDateUseDateFormat)
        //  指定全局Date字段的默认序列化格式。
        JSONObject.DEFFAULT_DATE_FORMAT = "yyyy-MM-dd HH:mm:ss";
        config.setSerializerFeatures(SerializerFeature.WriteDateUseDateFormat);
  /*      当上述三种方式或其中两种方式并存时，会采用优先级高的方式。三种方式的优先级：方式二 > 方式一 > 方式三， 即：在fastjson消息转换器里配置.setDateFormat("...")
                >在字段模型上添加 @JSONField(format = "...") >在fastjson消息转换器里配置.setSerializerFeatures(SerializerFeature.WriteDateUseDateFormat)；*/
        converter.setFastJsonConfig(config);
        converter.setDefaultCharset(Charset.forName("UTF-8"));
        List<MediaType> fastMediaTypes = new ArrayList<>();
        // 解决中文乱码问题，相当于在Controller上的@RequestMapping中加了个属性produces = "application/json"
        fastMediaTypes.add(MediaType.APPLICATION_JSON);
        converter.setSupportedMediaTypes(fastMediaTypes);
        converters.add(converter);

        /// -> String消息转换器
        StringHttpMessageConverter stringConverter = new StringHttpMessageConverter();
        stringConverter.setDefaultCharset(Charset.forName("UTF-8"));
        stringConverter.setSupportedMediaTypes(fastMediaTypes);

        // 将convert添加到converters当中.
        converters.add(converter);
        converters.add(stringConverter);
    }
}